package top.mingyi4cjh.cms.common.utils;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.Headers;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.http.HttpMethodName;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.transfer.MultipleFileDownload;
import com.qcloud.cos.transfer.TransferManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import top.mingyi4cjh.cms.common.error.BusinessException;
import top.mingyi4cjh.cms.common.error.EmBusinessError;

import java.io.File;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/**
 * 此类为系统调用腾讯云对象存储COS的API的方法集合类。
 * <p>
 * 本系统中使用的腾讯云对象存储COS的存储桶为私有读写，因此外界读取文件时仅可以使用临时链接进行读取。
 * </p>
 *
 * @author MingYi
 */
@Slf4j
@Component
@PropertySource("classpath:mingyi4cjh.properties")
public class CosUtil {
    /**
     * 腾讯云COS客户端单例，采用双重检查实现，请勿反射注入。
     */
    public volatile static COSClient cosClient;

    /**
     * 腾讯云COS证书单例，采用双重检查实现，请勿反射注入。
     */
    public volatile static COSCredentials cred;

    /**
     * 腾讯云COS客户都配置单例，采用双重检查实现，请勿反射注入。
     */
    public volatile static ClientConfig clientConfig;

    /**
     * 腾讯云API的secretId。
     * <p>请在resource/mingyi4cjh.properties中配置"top.mingyi4cjh.cos.secretId"，否则将无法使用。</p>
     */
    private static String secretId;

    /**
     * 腾讯云API的secretKey。
     * <p>请在resource/mingyi4cjh.properties中配置"top.mingyi4cjh.cos.secretKey"，否则将无法使用。</p>
     */
    private static String secretKey;

    /**
     * 希望使用的腾讯云对象存储COS存储桶的地域，请与bucketName对应的地域匹配，详细地域对应请参照腾讯云文档。
     * <p>请在resource/mingyi4cjh.properties中配置"top.mingyi4cjh.cos.region"，否则将无法使用。</p>
     */
    private static String regionName;

    /**
     * 希望使用的腾讯云对象存储COS存储桶的名称，请与regionName匹配，详细地域对应请参照腾讯云文档。
     * <p>请在resource/mingyi4cjh.properties中配置"top.mingyi4cjh.cos.bucketName"，否则将无法使用。</p>
     */
    private static String bucketName;

    @Value("${top.mingyi4cjh.cos.secretId}")
    private void setSecretId(String secretId) {
        CosUtil.secretId = secretId;
    }

    @Value("${top.mingyi4cjh.cos.secretKey}")
    private void setSecretKey(String secretKey) {
        CosUtil.secretKey = secretKey;
    }

    @Value("${top.mingyi4cjh.cos.region}")
    public void setRegionName(String regionName) {
        CosUtil.regionName = regionName;
    }

    @Value("${top.mingyi4cjh.cos.bucketName}")
    public void setBucketName(String bucketName) {
        CosUtil.bucketName = bucketName;
    }

    private CosUtil() {
    }

    /**
     * 获取腾讯云COS证书实例
     *
     * @return 腾讯云COS证书实例
     */
    public static COSCredentials getCred() {
        if (cred == null) {
            synchronized (CosUtil.class) {
                if (cred == null) {
                    return new BasicCOSCredentials(secretId, secretKey);
                }
            }
        }
        return cred;
    }

    /**
     * 获取腾讯云COS配置实例
     *
     * @return 腾讯云COS配置实例
     */
    public static ClientConfig getClientConfig() {
        if (clientConfig == null) {
            synchronized (CosUtil.class) {
                if (clientConfig == null) {
                    Region region = new Region(regionName);
                    return new ClientConfig(region);
                }
            }
        }
        return clientConfig;
    }

    /**
     * 获取腾讯云COS客户端实例
     *
     * @return 腾讯云COS客户端实例
     */
    public static COSClient getCosClient() {
        if (cosClient == null) {
            synchronized (CosUtil.class) {
                if (cosClient == null) {
                    COSCredentials cred = getCred();
                    ClientConfig clientConfig = getClientConfig();
                    clientConfig.setHttpProtocol(HttpProtocol.https);
                    return new COSClient(cred, clientConfig);
                }
            }
        }
        return cosClient;
    }

    /**
     * 上传文件到COS
     *
     * @param file 期望上传的文件
     * @param key  期望在COS中的存放位置
     */
    public static void uploadFile(File file, String key) {
        COSClient cosClient = getCosClient();
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, file);
        PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
        log.info("File [{}] uploaded to COS [{}] successfully.", file.getName(), key);
    }

    /**
     * 删除COS中的文件
     *
     * @param key 文件在COS中的存放位置
     */
    public static void deleteFile(String key) {
        COSClient cosClient = getCosClient();
        cosClient.deleteObject(bucketName, key);
        log.info("File at COS [{}] was deleted.", key);
    }

    /**
     * 获取文件的最后修改时间，可类比为上传时间
     *
     * @param key 文件在COS中存放的位置
     * @return 文件最后修改时间
     */
    public static Date getLastModifiedTime(String key) {
        COSClient cosClient = getCosClient();
        ObjectMetadata objectMetadata = cosClient.getObjectMetadata(bucketName, key);
        return objectMetadata.getLastModified();
    }

    /**
     * 获取文件的临时下载链接
     * <p>
     * 本方法中的params, requestMethod, token仅为生成临时链接时所携带的一定数据使用，可确定一个用户身份，防止否认。
     * </p>
     *
     * @param key           文件在COS中的存放位置
     * @param params        请求参数
     * @param requestMethod 请求方式
     * @param token         请求所带的token
     * @return 临时下载链接
     */
    public static String getObjectTemporaryUrl(String key, Map<String, String> params, String requestMethod, String token) {
        COSClient client = getCosClient();
        ClientConfig clientConfig = getClientConfig();
        // 设置签名过期时间(可选)，若未进行设置，则默认使用 ClientConfig 中的签名过期时间(1小时)
        // 这里设置签名在半个小时后过期
        Date expirationDate = new Date(System.currentTimeMillis() + 30L * 60L * 1000L);
        // 填写本次请求的头部
        Map<String, String> headers = new HashMap<>(16);
        // host 必填
        headers.put(Headers.HOST, clientConfig.getEndpointBuilder().buildGeneralApiEndpoint(bucketName));
        headers.put("token", token);
        headers.put("requestMethod", requestMethod);
        // 请求的 HTTP 方法，上传请求用 PUT，下载请求用 GET，删除请求用 DELETE
        HttpMethodName method = HttpMethodName.GET;
        URL url = client.generatePresignedUrl(bucketName, key, expirationDate, method, headers, params);
        log.info("File at COS [{}] was shard.", key);
        return url.toString();
    }

    /**
     * 下载COS一个文件夹下的所有文件
     *
     * @param cosPath    COS中的路径（COS中文件Key的前缀）
     * @param targetPath 将文件下载到本地的目标路径
     */
    public static void downDirFile(String cosPath, String targetPath) {
        // 使用高级接口必须先保证本进程存在一个 TransferManager 实例，如果没有则创建
        // 详细代码参见腾讯云对象存储COS的SDK文档：高级接口 -> 示例代码：创建 TransferManager
        TransferManager transferManager = createTransferManager();
        try {
            // 返回一个异步结果download, 可同步的调用waitForUploadResult等待download结束.
            MultipleFileDownload download = transferManager.downloadDirectory(bucketName, cosPath, new File(targetPath));
            // 或者阻塞等待完成
            download.waitForCompletion();
            // 确定本进程不再使用 transferManager 实例之后，关闭之
            // 详细代码参见腾讯云对象存储COS的SDK文档：高级接口 -> 示例代码：关闭 TransferManager
            shutdownTransferManager(transferManager);
        } catch (CosClientException | InterruptedException e) {
            log.error("COS dir [{}] download to [{}] fail. ErrMsg: {}", cosPath, targetPath, e);
            throw new BusinessException(EmBusinessError.FILE_PROCESS_ERROR);
        }
        log.info("COS dir [{}] download to [{}] successfully.", cosPath, targetPath);
    }

    /**
     * 创建腾讯云对象存储COS的TransferManager实例，这个实例用来后续调用高级接口
     * <p>腾讯云文档中提供demo如下，无法通过阿里巴巴开发规约检查，认为可能消耗过多资源进而OOM，但我认为指定了nThreads后不会出现消耗资源过多的情况。</p>
     * {@code ExecutorService threadPool = Executors.newFixedThreadPool(16);TransferManager transferManager = new TransferManager(cosClient, threadPool);}
     *
     * @return TransferManager实例
     */
    private static TransferManager createTransferManager() {
        // 创建一个 COSClient 实例，这是访问 COS 服务的基础实例。
        // 详细代码参见腾讯云对象存储COS的SDK文档: 简单操作 -> 创建 COSClient
        COSClient cosClient = getCosClient();
        // 自定义线程池大小，建议在客户端与 COS 网络充足（例如使用腾讯云的 CVM，同地域上传 COS）的情况下，设置成16或32即可，可较充分的利用网络资源
        // 对于使用公网传输且网络带宽质量不高的情况，建议减小该值，避免因网速过慢，造成请求超时。
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("cos-thread-pool").build();
        ExecutorService threadPool = Executors.newFixedThreadPool(16);
        TransferManager transferManager = new TransferManager(cosClient, threadPool);

        // 传入一个 threadpool, 若不传入线程池，默认 TransferManager 中会生成一个单线程的线程池。
        return transferManager;
    }

    /**
     * 关闭腾讯云对象存储COS的TransferManager实例
     *
     * @param transferManager TransferManger实例
     */
    private static void shutdownTransferManager(TransferManager transferManager) {
        // 指定参数为 true, 则同时会关闭 transferManager 内部的 COSClient 实例。
        // 指定参数为 false, 则不会关闭 transferManager 内部的 COSClient 实例。
        transferManager.shutdownNow(false);
    }
}